#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/soundcard.h>
#include <cmath>
#include "play.h"
#ifdef OUTPUT_WAVE
#include <fstream>
#endif
#include "my_math.h"

void play_piano(double, double, double);
void play_accordion(double, double, double);
void play_violin(double, double, double);

play_t play;

void play_t::operator()(int instrument,
                        int freq_index,
                        int time_ms,
                        int volume)
{
    if (m_freq_list == nullptr)
    {
        perror("freq_listis NULL, please set freq_list\n");
        exit(-1);
    }
    auto play_instrument = play_instruments[instrument];
    play_instrument(m_freq_list[freq_index], time_ms / 1000.0, volume / 100.0);
}

play_t::play_t()
    : m_freq_list(nullptr)
{
    if ((m_play_device_fd = open(PLAY_DEVICE, O_WRONLY)) < 0)
    {
        fprintf(stderr, "Open %s failed: %s\n",
                PLAY_DEVICE, strerror(errno));
        exit(-1);
    }
    int ioctl_val;
    if (ioctl(m_play_device_fd, SNDCTL_DSP_SETFMT, &(ioctl_val = DATA_FMT)) < 0 ||
        ioctl(m_play_device_fd, SNDCTL_DSP_CHANNELS, &(ioctl_val = CHANNEL_NUM)) < 0 ||
        ioctl(m_play_device_fd, SNDCTL_DSP_SPEED, &(ioctl_val = SAMPLING_RATE)) < 0)
    {
        fprintf(stderr, "initialize play failed\n");
        exit(-1);
    }
    play_instruments[0] = play_piano;
    play_instruments[1] = play_accordion;
    play_instruments[2] = play_violin;
    return;
}

play_t::~play_t()
{
    if (close(m_play_device_fd))
    {
        fprintf(stderr, "close %s failed: %s\n",
                PLAY_DEVICE, strerror(errno));
        exit(-1);
    }
}

int play_t::play_buf(void *data_buf_, size_t buf_size)
{
    char *data_buf = (char *)data_buf_;
    size_t writtenSize = 0;
    while (buf_size > 0)
    {
        if ((writtenSize = write(m_play_device_fd, data_buf, buf_size)) < 0)
        {
            perror("Write sound device file failed");
            return -1;
        }
        buf_size -= writtenSize;
        data_buf += writtenSize;
    }
    return 0;
}

void play_t::set_freq_list(double *freq_list)
{
    m_freq_list = freq_list;
}

void set_freq_list(double *freq_list)
{
    play.set_freq_list(freq_list);
}

#define x2I(x) (0.3675 * 0.3675 * x * x / 100)
#define BUF_SIZE SAMPLING_RATE * 20
static unsigned char buf[SAMPLING_RATE * 20];

double AM_piano2[] = {
#include "AM_piano2.array"
};
inline double AM_piano_2(size_t ticks)
{
    if (ticks < sizeof(AM_piano2) / sizeof(double))
        return AM_piano2[ticks];
    return 0;
}

double AM_piano1[] = {
#include "AM_piano1.array"
};
inline double AM_piano_1(size_t ticks)
{
    if (ticks < sizeof(AM_piano1) / sizeof(double))
        return AM_piano1[ticks];
    return 0.194;
}

double AM_piano0[] = {
#include "AM_piano0.array"
};
inline double AM_piano_0(size_t ticks)
{
    if (ticks < sizeof(AM_piano0) / sizeof(double))
        return AM_piano0[ticks];
    return 0;
}

void play_piano(double frequency_base, double time, double AM_base)
{
#ifdef OUTPUT_WAVE
    std::ofstream fout("wave-piano.txt");
#endif
    static const double ratio[] = {0.9992, 0.9998, 6.9967};
    static const double offset[] = {-0.65, -0.13, -2.72};
    static const double I[] = {1, x2I(41), x2I(21)};
    static double (*AM[])(size_t ticks) = {AM_piano_0,
                                           AM_piano_1,
                                           AM_piano_2};
    double freq[] = {frequency_base * ratio[0] + offset[0],
                     frequency_base * ratio[1] + offset[1],
                     frequency_base * ratio[2] + offset[2]};
    double omega[] = {freq[0] * 2 * M_PI,
                      freq[1] * 2 * M_PI,
                      freq[2] * 2 * M_PI};
    static double data[3];
    size_t ticks = time * SAMPLING_RATE;
    double t = 0;
    for (int i = 0; i < ticks; i++)
    {
        t = (double)i / SAMPLING_RATE;
        data[2] = my_sin(omega[2] * t) * AM[2](i);
        data[1] = my_sin(omega[1] * t + I[2] * data[2]) * AM[1](i);
        data[0] = my_sin(omega[0] * t + I[1] * data[1]) * AM[0](i);
        buf[i] = (unsigned char)(AM_base * data[0] * 128 + 128);
#ifdef OUTPUT_WAVE
        fout << (int)buf[i] << "\n";
#endif
    }
    play.play_buf(buf, ticks);
#ifdef OUTPUT_WAVE
    fout.close();
#endif
}

const double accordion_t0 = 0.02;
double inline AM_accordion(double t, double time)
{
    if (t < accordion_t0)
        return t * t / (accordion_t0 * accordion_t0);
    if (t > time - accordion_t0)
    {
        t = time - t;
        return t * t / (accordion_t0 * accordion_t0);
    }
    return 1;
}

void play_accordion(double frequency_base, double time, double AM_base)
{
#ifdef OUTPUT_WAVE
    std::ofstream fout("wave-accordion.txt");
#endif
    static const double ratio[] = {1, 0.49999, 0.49998, 2.0003, 1.0003, 6.0029};
    static const double offset[] = {0, -0.06, -0.13, 0.26, 0.26, 2.33};
    double freq[] = {frequency_base * ratio[0] + offset[0],
                     frequency_base * ratio[1] + offset[1],
                     frequency_base * ratio[2] + offset[2],
                     frequency_base * ratio[3] + offset[3],
                     frequency_base * ratio[4] + offset[4],
                     frequency_base * ratio[5] + offset[5]};
    double omega[] = {freq[0] * 2 * M_PI,
                      freq[1] * 2 * M_PI,
                      freq[2] * 2 * M_PI,
                      freq[3] * 2 * M_PI,
                      freq[4] * 2 * M_PI,
                      freq[5] * 2 * M_PI};
    double data[6] = {0, 0, 0, 0, 0, 0};
    size_t ticks = time * SAMPLING_RATE;
    double t = 0;
    const double r = 0.85;
    for (int i = 0; i < ticks; i++)
    {
        t = (double)i / SAMPLING_RATE;
        data[5] = my_sin(omega[5] * t + x2I(20) * data[5]);
        data[4] = my_sin(omega[4] * t + x2I(25) * data[5]);
        data[3] = my_sin(omega[3] * t + x2I(33) * data[4]);
        data[2] = my_sin(omega[2] * t);
        data[1] = my_sin(omega[1] * t + x2I(26) * data[2]);
        data[0] = my_sin(omega[0] * t + x2I(75) * data[1]);
        buf[i] = (unsigned char)(AM_base * AM_accordion((double)i / SAMPLING_RATE, time) * (data[0] * (1 - r) + data[3] * r) * 128 +128);
#ifdef OUTPUT_WAVE
        fout << (int)buf[i] << "\n";
#endif
    }
    play.play_buf(buf, ticks);
#ifdef OUTPUT_WAVE
    fout.close();
#endif
}

const double violin_t0 = 0.02;
double inline AM_violin(double t, double time)
{
    if (t < time*0.2)
        return t/time/0.2;
    if (t > time*0.2)
    {
        t = time - t;
        return t / time / 0.8;
    }
    return 1;
}

void play_violin(double frequency_base, double time, double AM_base)
{
#ifdef OUTPUT_WAVE
    std::ofstream fout("wave-violin.txt");
#endif
    static const double ratio[] = {1, 0.29999, 0.19998, 1.0003, 1.0003, 1.0029};
    static const double offset[] = {0, -0.06, -0.13, 0.26, 0.26, 2.33};
    double freq[] = {frequency_base * ratio[0] + offset[0],
                     frequency_base * ratio[1] + offset[1],
                     frequency_base * ratio[2] + offset[2],
                     frequency_base * ratio[3] + offset[3],
                     frequency_base * ratio[4] + offset[4],
                     frequency_base * ratio[5] + offset[5]};
    double omega[] = {freq[0] * 2 * M_PI,
                      freq[1] * 2 * M_PI,
                      freq[2] * 2 * M_PI,
                      freq[3] * 2 * M_PI,
                      freq[4] * 2 * M_PI,
                      freq[5] * 2 * M_PI};
    double data[6] = {0, 0, 0, 0, 0, 0};
    size_t ticks = time * SAMPLING_RATE;
    double t = 0;
    const double r = 0.85;
    for (int i = 0; i < ticks; i++)
    {
        t = (double)i / SAMPLING_RATE;
        data[5] = my_sin(omega[5] * t + x2I(20) * data[5]);
        data[4] = my_sin(omega[4] * t + x2I(25) * data[5]);
        data[3] = my_sin(omega[3] * t + x2I(33) * data[4]);
        data[2] = my_sin(omega[2] * t);
        data[1] = my_sin(omega[1] * t + x2I(26) * data[2]);
        data[0] = my_sin(omega[0] * t + x2I(75) * data[1]);
        buf[i] = (unsigned char)(AM_base * AM_violin((double)i / SAMPLING_RATE, time) * (data[0] * (1 - r) + data[3] * r) * 128 + 128);
#ifdef OUTPUT_WAVE
        fout << (int)buf[i] << "\n";
#endif
    }
    play.play_buf(buf, ticks);
#ifdef OUTPUT_WAVE
    fout.close();
#endif
}
